{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Note\n",
    "\n",
    "Please view the [README](https://github.com/eclipse/deeplearning4j-examples/blob/master/tutorials/README.md) to learn about installing, setting up dependencies, and importing notebooks in Zeppelin"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Background\n",
    "\n",
    "Neural network hyperparameters are parameters set prior to training. They include the learning rate, batch size, number of epochs, regularization, weight initialization, number of hidden layers, number of nodes, and etc. Unlike the weights and biases of the nodes of the neural network, they cannot be estimated directly using the data. Setting an optimal or near-optimal configuration of the hyperparameters can significantly affect neural network performance. Thus, time should be set aside to tune these hyperparameters.\n",
    "\n",
    "Deeplearning4j (DL4J) provides functionality to do exactly this task. Arbiter was created explicitly for tuning neural network models and is part of the DL4J suite of deep learning tools. In this tutorial, we will show an example of using Arbiter to tune the learning rate and the number of hidden nodes or layer size of a neural network model. We will use the MNIST dataset (images of handwritten digits) to train the neural network. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "import org.deeplearning4j.api.storage.StatsStorage\n",
    "import org.deeplearning4j.arbiter.MultiLayerSpace\n",
    "import org.deeplearning4j.arbiter.layers.DenseLayerSpace\n",
    "import org.deeplearning4j.arbiter.layers.OutputLayerSpace\n",
    "import org.deeplearning4j.arbiter.optimize.api.CandidateGenerator\n",
    "import org.deeplearning4j.arbiter.optimize.api.OptimizationResult\n",
    "import org.deeplearning4j.arbiter.optimize.api.ParameterSpace\n",
    "import org.deeplearning4j.arbiter.optimize.api.data.DataProvider\n",
    "import org.deeplearning4j.arbiter.data.MnistDataProvider\n",
    "import org.deeplearning4j.arbiter.optimize.api.saving.ResultReference\n",
    "import org.deeplearning4j.arbiter.optimize.api.saving.ResultSaver\n",
    "import org.deeplearning4j.arbiter.optimize.api.score.ScoreFunction\n",
    "import org.deeplearning4j.arbiter.optimize.api.termination.MaxCandidatesCondition\n",
    "import org.deeplearning4j.arbiter.optimize.api.termination.MaxTimeCondition\n",
    "import org.deeplearning4j.arbiter.optimize.api.termination.TerminationCondition\n",
    "import org.deeplearning4j.arbiter.optimize.config.OptimizationConfiguration\n",
    "import org.deeplearning4j.arbiter.optimize.generator.RandomSearchGenerator\n",
    "import org.deeplearning4j.arbiter.optimize.parameter.continuous.ContinuousParameterSpace\n",
    "import org.deeplearning4j.arbiter.optimize.parameter.integer.IntegerParameterSpace\n",
    "import org.deeplearning4j.arbiter.optimize.runner.IOptimizationRunner\n",
    "import org.deeplearning4j.arbiter.optimize.runner.LocalOptimizationRunner\n",
    "import org.deeplearning4j.arbiter.saver.local.FileModelSaver\n",
    "import org.deeplearning4j.arbiter.scoring.impl.TestSetAccuracyScoreFunction\n",
    "import org.deeplearning4j.arbiter.task.MultiLayerNetworkTaskCreator\n",
    "import org.deeplearning4j.datasets.iterator.MultipleEpochsIterator\n",
    "import org.deeplearning4j.datasets.iterator.impl.MnistDataSetIterator\n",
    "import org.deeplearning4j.nn.multilayer.MultiLayerNetwork\n",
    "import org.deeplearning4j.nn.weights.WeightInit\n",
    "import org.nd4j.linalg.activations.Activation\n",
    "import org.nd4j.linalg.dataset.api.iterator.DataSetIterator\n",
    "import org.nd4j.linalg.lossfunctions.LossFunctions\n",
    "import org.nd4j.shade.jackson.annotation.JsonProperty\n",
    "import org.nd4j.linalg.factory.Nd4j\n",
    "import org.nd4j.linalg.cpu.nativecpu.CpuAffinityManager\n",
    "\n",
    "\n",
    "import java.io.File\n",
    "import java.io.IOException\n",
    "import java.util.List\n",
    "import java.util.Map\n",
    "import java.util.concurrent.TimeUnit\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our goal of this tutorial is to tune the learning rate and the layer size. We can start by setting up the parameter space of the learning rate and the layer size. We will consider values between 0.0001 and 0.1 for the learning rate and integer values between 16 and 256 for the layer size. \n",
    "\n",
    "Next, we set up a MultiLayerSpace, which is similar in structure to the MultiLayerNetwork class we've seen below. Here, we can set the hyperparameters of the neural network model. However, we can set the learning rate and the number of hidden nodes using the ParameterSpaces we've initialized before and not a set value like the other hyperparameters.\n",
    "\n",
    "Lastly, we use the CandidateGenerator class to configure how candidate values of the learning rate and the layer size will be generated. In this tutorial, we will use random search; thus, values for the learning rate and the layer size will be generated uniformly within their ranges."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val learningRateHyperparam  = new ContinuousParameterSpace(0.0001, 0.1)\n",
    "val layerSizeHyperparam  = new IntegerParameterSpace(16,256)            \n",
    "\n",
    "\n",
    "val hyperparameterSpace  = new MultiLayerSpace.Builder()\n",
    "    //These next few options: fixed values for all models\n",
    "    .weightInit(WeightInit.XAVIER)\n",
    "    .regularization(true)\n",
    "    .l2(0.0001)\n",
    "    //Learning rate hyperparameter: search over different values, applied to all models\n",
    "    .learningRate(learningRateHyperparam)\n",
    "    .addLayer( new DenseLayerSpace.Builder()\n",
    "            //Fixed values for this layer:\n",
    "            .nIn(784)  //Fixed input: 28x28=784 pixels for MNIST\n",
    "            .activation(Activation.LEAKYRELU)\n",
    "            //One hyperparameter to infer: layer size\n",
    "            .nOut(layerSizeHyperparam)\n",
    "            .build())\n",
    "    .addLayer( new OutputLayerSpace.Builder()\n",
    "            .nOut(10)\n",
    "            .activation(Activation.SOFTMAX)\n",
    "            .lossFunction(LossFunctions.LossFunction.MCXENT)\n",
    "            .build())\n",
    "    .build()\n",
    "    \n",
    "val candidateGenerator = new RandomSearchGenerator(hyperparameterSpace, null)   \n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " \n",
    "\n",
    "To obtain the data, we will use the built-in MnistDataProvider class and use two training epochs or complete passes through the data and a batch size of 64 for training."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val nTrainEpochs = 2\n",
    "val batchSize = 64\n",
    "\n",
    "val dataProvider = new MnistDataProvider(nTrainEpochs, batchSize)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " \n",
    "\n",
    "We've set how we are going to generate new values of the two hyperparameters we are considering but there still remains the question of how to evaluate them. We will use the accuracy score metric to evaluate different configurations of the hyperparameters so we initialize a TestSetAccuracyScoreFunction."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val scoreFunction = new TestSetAccuracyScoreFunction()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We also want to set how long the hyperparameter search will last. There are infinite configurations of the learning rate and hidden layer size, since the learning rate space is continuous. Thus, we set a termination condition of 15 minutes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val terminationConditions = { new MaxTimeCondition(15, TimeUnit.MINUTES)}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To save the best model, we can set the directory to save it in."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val baseSaveDirectory = \"arbiterExample/\"\n",
    "val f = new File(baseSaveDirectory)\n",
    "if(f.exists()) f.delete()\n",
    "f.mkdir()\n",
    "val modelSaver = new FileModelSaver(baseSaveDirectory)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Given all the configurations we have already set, we need to put them together using the OptimizationConfiguration. To execute the hyperparameter search, we initialize an IOptimizaitonRunner using the OptimizationConfiguration. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val configuration = new OptimizationConfiguration.Builder()\n",
    "                .candidateGenerator(candidateGenerator)\n",
    "                .dataProvider(dataProvider)\n",
    "                .modelSaver(modelSaver)\n",
    "                .scoreFunction(scoreFunction)\n",
    "                .terminationConditions(terminationConditions)\n",
    "                .build()\n",
    "\n",
    "val runner = new LocalOptimizationRunner(configuration, new MultiLayerNetworkTaskCreator())\n",
    "\n",
    "//Start the hyperparameter optimization\n",
    "\n",
    "runner.execute()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " \n",
    "Lastly, we can print out the details of the best model and the results.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "autoscroll": "auto"
   },
   "outputs": [],
   "source": [
    "val s = \"Best score: \" + runner.bestScore() + \"\\n\" + \"Index of model with best score: \" + runner.bestScoreCandidateIndex() + \"\\n\" + \"Number of configurations evaluated: \" + runner.numCandidatesCompleted() + \"\\n\"\n",
    "println(s)\n",
    "\n",
    "\n",
    "//Get all results, and print out details of the best result:\n",
    "val indexOfBestResult = runner.bestScoreCandidateIndex()\n",
    "val allResults = runner.getResults()\n",
    "\n",
    "val bestResult = allResults.get(indexOfBestResult).getResult()\n",
    "val bestModel = bestResult.getResult().asInstanceOf[MultiLayerNetwork]\n",
    "\n",
    "\n",
    "println(\"\\n\\nConfiguration of best model:\\n\")\n",
    "println(bestModel.getLayerWiseConfigurations().toJson())"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Spark 2.0.0 - Scala 2.11",
   "language": "scala",
   "name": "spark2-scala"
  },
  "language_info": {
   "codemirror_mode": "text/x-scala",
   "file_extension": ".scala",
   "mimetype": "text/x-scala",
   "name": "scala",
   "pygments_lexer": "scala",
   "version": "2.11.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
